Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

31장. 의존성이 있는 코드 테스트하기

30장에서 테스트의 기본기를 배웠습니다.
계산기처럼 혼자 동작하는 코드는 테스트하기 쉬웠습니다.

그런데 실제 백엔드 코드는
다른 객체에 기대어 동작합니다.

예를 들어 회원 가입 서비스는
데이터베이스(Repository)에 의존합니다.

이럴 때 진짜 데이터베이스를 쓰면
테스트가 느리고 불안정해집니다.

그래서 우리는 “가짜 대역“을 씁니다.
이 장에서 그 방법을 배웁니다.


31.1 Test Double

영화에서 위험한 장면은
배우 대신 스턴트 대역이 연기합니다.

테스트에서도 마찬가지입니다.
진짜 객체 대신 쓰는 “대역“을
테스트 더블(Test Double)이라고 합니다.

테스트 더블에는 여러 종류가 있습니다.

종류한 줄 설명
Dummy그냥 자리만 채우는 가짜
Stub정해진 답만 돌려주는 가짜
Fake간단하지만 실제로 동작하는 가짜
Mock호출을 기록하고 검증하는 가짜

이제 하나씩 살펴봅니다.

먼저 예제에 쓸 인터페이스를 정합니다. (8장 참고)

interface UserRepository {
    fun findById(id: Long): User?
    fun save(user: User): User
}

31.2 Dummy

더미(Dummy)는
그냥 자리만 채우는 가짜입니다.

호출되지 않을 것을 알지만,
매개변수 자리를 채워야 할 때 씁니다.

class DummyUserRepository : UserRepository {
    override fun findById(id: Long): User? = null
    override fun save(user: User): User = user
}

내용은 거의 비어 있습니다.
“필요하지만 실제로 쓰이지는 않는” 역할입니다.


31.3 Stub

스텁(Stub)은
“정해진 답만 돌려주는” 가짜입니다.

특정 상황을 만들고 싶을 때 유용합니다.

class StubUserRepository : UserRepository {
    override fun findById(id: Long): User? {
        return User(id = 1, name = "홍길동")   // 늘 같은 답
    }
    override fun save(user: User): User = user
}

“회원이 존재하는 상황“을
간단히 만들어 낼 수 있습니다.


31.4 Fake

페이크(Fake)는
간단하지만 실제로 동작하는 가짜입니다.

예를 들어 진짜 데이터베이스 대신
메모리(Map)에 저장하는 저장소를 만들 수 있습니다.

class FakeUserRepository : UserRepository {
    private val storage = mutableMapOf<Long, User>()

    override fun findById(id: Long): User? = storage[id]

    override fun save(user: User): User {
        storage[user.id] = user
        return user
    }
}

저장하면 진짜로 저장되고,
조회하면 저장한 값이 나옵니다.
다만 메모리에서만 동작하므로
빠르고 가볍습니다.

Fake는 백엔드 테스트에서 특히 자주 쓰입니다.


31.5 Mock

목(Mock)은
“어떤 호출이 일어났는지“를 기록하고
그것을 검증하는 데 쓰는 가짜입니다.

“save가 정말 호출됐는가?” 같은 것을
확인하고 싶을 때 유용합니다.

Mock은 보통 손으로 만들기보다
전문 라이브러리로 만듭니다.
코틀린에서는 MockK를 많이 씁니다.


31.6 Repository를 Fake로 교체하기

이제 실제 테스트를 해 봅시다.

검사할 서비스입니다. (Service는 36장에서 자세히 다룹니다)

class UserService(
    private val userRepository: UserRepository
) {
    fun register(name: String): User {
        val user = User(id = 1, name = name)
        return userRepository.save(user)
    }
}

UserServiceUserRepository에 의존합니다.
테스트에서는 이 자리에 Fake를 넣습니다.

@Test
fun `회원을 등록하면 저장된다`() {
    // Given
    val fakeRepository = FakeUserRepository()
    val userService = UserService(fakeRepository)

    // When
    val user = userService.register("홍길동")

    // Then
    assertEquals("홍길동", fakeRepository.findById(user.id)?.name)
}

진짜 데이터베이스 없이도
서비스 로직을 완전히 검증할 수 있습니다.

이것이 가능한 이유는
UserService가 구현체가 아니라
인터페이스에 의존하기 때문입니다.
(8장에서 배운 원리입니다.)


31.7 MockK 맛보기

MockK는 코틀린용 목 라이브러리입니다.

dependencies {
    testImplementation("io.mockk:mockk:1.13.11")
}

간단한 사용 예를 봅시다.

@Test
fun `조회 결과를 지정한다`() {
    val repository = mockk<UserRepository>()

    // findById(1)을 부르면 이 값을 돌려주도록 지정
    every { repository.findById(1) } returns User(1, "홍길동")

    val user = repository.findById(1)

    assertEquals("홍길동", user?.name)
}

every { ... } returns ...
“이 호출엔 이 답을 줘라“라고 지정합니다.

호출 여부를 검증할 수도 있습니다.

verify { repository.findById(1) }

MockK는 강력하지만,
Fake로 충분한 경우에는
Fake가 더 읽기 쉬울 때도 많습니다.


31.8 테스트하기 좋은 코드의 구조

여기서 중요한 깨달음이 있습니다.

테스트하기 어려운 코드는
대개 구조가 좋지 않은 코드입니다.

반대로,
테스트하기 쉬운 코드는 대개 구조가 좋습니다.

테스트하기 좋은 코드의 특징은 이렇습니다.

  • 의존성을 생성자로 받는다 (36장 참고)
  • 구현체가 아니라 인터페이스에 의존한다
  • 하나의 객체가 하나의 책임만 가진다

“테스트하기 쉽게 짜라“는 말은
사실 “좋은 구조로 짜라“는 말과 같습니다.

이 감각은 11부(백엔드 구조)에서
본격적으로 다룹니다.


31장을 마치며

이 장에서 우리는 다음을 배웠습니다.

  • 테스트 더블(Dummy, Stub, Fake, Mock)의 종류와 차이
  • Fake Repository로 의존성을 교체하는 법
  • 인터페이스 덕분에 교체가 가능하다는 점
  • MockK로 목을 만드는 기본 사용법
  • 테스트하기 좋은 코드가 곧 좋은 구조라는 점

이것으로 10부(프로젝트와 테스트)를 마칩니다.
다음 11부에서는 지금까지 배운 것을 모아
백엔드 애플리케이션의 구조를 만들어 봅니다.